/* This file is part of FireEscape.
 *
 * FireEscape is the intellectual work of Phillip Cheng & Allen Park.
 * All future releases or modifications of this program must include this
 * attribuation as according to the Creative Commons BY-SA license.
 *
 * FireEscape is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * FireEscape is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with FireEscape.  If not, see <http://www.gnu.org/licenses/>.
 *
 * All modifications to this program should include this original statement.
 *
 * If you modify, distribute, or use the program , please contact
 * fireescape@kloudvine.com
 * http://code.google.com/p/fireescape/
 *
 * If you are interested in using FireEscape for your structure, please contact us
 * for support.
 *
 * If you do use FireEscape, please consider donating to the project.
 */
package fireescape;

import java.util.ArrayList;
import java.util.logging.*;

/**
 * This class contains the Bot AI code
 * @author chengP
 */
public class BotAI {

    private static ArrayList<Bot> bots = new ArrayList<Bot>();
    public static final double timePerIteration = 1;        //shouldn't be bigger than 2
    public static final int botRadius = 2;
    public static final int deathTime = 500;
    public static int iterations;
    public static int botsEscaped;
    public static int botsDied;
    public static final double defaultVelocity = 3;
    public static double defaultDistance;

    /**
     * Constructs the botAI
     * @param botList The list of bots
     */
    public BotAI() {
        iterations = 0;
        botsEscaped = 0;
        botsDied = 0;
        defaultDistance = defaultVelocity * timePerIteration;
    }

    /**
     * Resets the botAI
     */
    public static void reset() {
        iterations = 0;
        botsEscaped = 0;
        botsDied = 0;
        bots = new ArrayList<Bot>();
    }

    /**
     * Generates the neighbors and moves the bots.
     */
    public static void run() {
        iterations++;
        generateNeighbors();
        moveBots();
        checkForDeath();
    }

    /**
     * Adds a bot
     * @param bot The new bot
     */
    public static void addBot(Bot bot) {
        bots.add(bot);
        Map.getRoom(bot.getRoomID()).addBot(bot);
    }

    /**
     * Returns a bot
     * @param botNum The bot ID
     * @return The bot
     */
    public static Bot getBot(int botNum) {
        return bots.get(botNum);
    }

    /**
     * Returns the number of bots
     * @return The number of bots
     */
    public static int getNumBots() {
        return bots.size();
    }

    /**
     * Returns the distance between a and b
     * @param a The first bot
     * @param b The second bot
     * @return The distance between a and b
     */
    public static double distance(Bot a, Bot b) {
        return FireEscapeGUI.distance(a.getX(), a.getY(), b.getX(), b.getY());
    }

    /**
     * Calculates the angle between a and b
     * @param a The first bot
     * @param b The second bot
     * @return The angle formed by b with respect to a
     */
    public static double calculateBotAngle(Bot a, Bot b) {
        return calculateAngle(a.getX(), a.getY(), b.getX(), b.getY());
    }

    /**
     * Calculates the angle between (x1, y1) and (x2, y2)
     * @param x1 The first x coordinate
     * @param y1 The first y coordinate
     * @param x2 The second x coordinate
     * @param y2 The second y coordinate
     * @return The angle between (x1, y1) and (x2, y2) between 0 and 2PI
     */
    public static double calculateAngle(double x1, double y1, double x2, double y2) {
        if (x1 == x2 && y1 == y2) {
            Logger.getLogger("FireEscapeLog").log(Level.FINE, "Angle does not exist");
            //System.out.println("BotAI calculateAngle");
        }
        if (x1 == x2) {
            if (y1 < y2) {
                return Math.PI / 2;
            } else {
                return 3 * Math.PI / 2;
            }
        }
        double angle = Math.atan((y2 - y1) / (x2 - x1));
        if (x2 < x1) {
            angle += Math.PI;
        }
        if (angle < 0) {
            angle += 2 * Math.PI;
        }
        return angle;
    }

    /**
     * Returns the angle between the bot and the destination
     * @param b The bot
     * @return The angle between the bot and the destination
     */
    public static double getMaxAdvantageousAngle(Bot b) {
        if (b.onStairs()) {
            return Math.PI;
        } else if (b.exiting()) {
            Room room = Map.getRoom(b.getRoomID());
            if (room.getRoomID() != 0 && !b.exitIsStairs()) {
                return BotAI.calculateAngle(b.getX(), b.getY(), room.getCenterX(), room.getCenterY());
            } else {
                /*if (b.exitIsStairs()) {
                return 0;
                }*/
                Exit waypoint = b.getCurrentWaypoint();
                double perpen = waypoint.perpendicularToExit();
                //double desiredDirection = Math.atan((b.getY() - waypoint.getCenterY()) / (b.getX() - waypoint.getCenterX()));
                double difference = Math.abs(perpen - b.getIdealDirection());
                if (difference > Math.PI) {
                    difference = 2 * Math.PI - difference;
                }
                if (difference < 0 || difference > Math.PI) {
                    Logger.getLogger("FireEscapeLog").log(Level.FINE, "Angle difference is not in correct range");
                    //System.out.println("BotAI getMaxAdvantageousAngle");
                }
                return (difference < Math.PI / 2 ? perpen : perpen + Math.PI);
            }
        }
        return calculateAngle(b.getX(), b.getY(), b.getDestX(), b.getDestY());
    }

    /**
     * Returns the nearest possible direction the bot can go without crashing. Also sets the proper velocity.
     * @param b The bot
     * @param direction The angle between the bot and the destination
     * @return The nearest possible direction the bot can go without crashing
     */
    public static double getNearestPossibleDirection2(Bot b, double bestDirection) {
        boolean dirswitch = true;
        double direction = bestDirection;
        int power = 1;
        b.setVelocity(defaultVelocity);
        while (!canBotMoveInDirection(b, direction)) {
            if (dirswitch) {
                direction += Math.PI / 180 * power;
            } else {
                direction -= Math.PI / 180 * power;
            }
            if (power >= 360) {
                /*b.setVelocity(0);
                return 0;*/
                Bot nearNeigh = b.getNearestNeighbor();     //if it's in this loop, the nearest neighbor cannot be null
                if (nearNeigh == null) {
                    //sigh canBotMoveInDirection(b, direction);
                    Logger.getLogger("FireEscapeLog").log(Level.FINE, "Nearest neighbor is null");
                    //System.out.println("BotAI getNearestPossibleDirection2'");
                    b.setVelocity(0);
                    return 0;
                }
                double distance = Math.min(distance(b, nearNeigh), Map.getRoom(b.getRoomID()).minDistanceToWall(b));
                double velocity = distance / timePerIteration;
                b.setVelocity(velocity);// + defaultVelocity * 0.4);
                direction = calculateBotAngle(b, nearNeigh) + ((Math.PI / 2 + Math.random() * Math.PI));// + Math.PI;
                return direction;
                //return bestDirection;
            }
            power++;
            dirswitch = !dirswitch;
        }
        return direction;
    }

    /**
     * Returns the nearest possible direction the bot can go without crashing
     * @param b The bot
     * @param direction The angle between the bot and the destination
     * @return The nearest possible direction the bot can go without crashing
     */
    public static double getNearestPossibleDirection(Bot b, double bestDirection) {//This fix can only work when the blind running through exits is fixed and they can't start on top of each other.
        boolean dirswitch = true;
        double direction = bestDirection;
        int power = 1;
        b.setVelocity(defaultVelocity);
        while (!canBotMoveInDirection(b, direction)) {
            if (dirswitch) {
                direction += Math.PI / 180 * power;
            } else {
                direction -= Math.PI / 180 * power;
            }
            if (power >= 360) {
                /*b.setVelocity(0);
                return 0;*/
                Bot nearNeigh = b.getNearestNeighbor();     //if it's in this loop, the nearest neighbor cannot be null
                if (nearNeigh == null) {
                    //sigh canBotMoveInDirection(b, direction);
                    Logger.getLogger("FireEscapeLog").log(Level.FINE, "Nearest neighbor is null");
                   // System.out.println("BotAI getNearestPossibleDirection1");
                    b.setVelocity(0);
                    return 0;
                }
                double distance = Math.min(distance(b, nearNeigh) - 2 * botRadius, minWallDistance(b));
                double velocity = distance / timePerIteration;
                if (velocity < 0) {
                    velocity = 0;
                }
                b.setVelocity(velocity);// + defaultVelocity * 0.4);
                double randomAngle = calculateBotAngle(b, nearNeigh) + ((Math.PI / 2 + Math.random() * Math.PI));
                if (!canBotMoveInDirection(b, randomAngle)) {
                    Logger.getLogger("FireEscapeLog").log(Level.FINE, "Changing velocity to maximum did not work");
                   // System.out.println("BotAI getNearestPossibleDirection2 " + b);
                    distance = Math.min(distance(b, nearNeigh) - 2 * botRadius, minWallDistance(b));
                    canBotMoveInDirection(b, randomAngle);   //sigh
                }
                return randomAngle;// + Math.PI;
            }
            power++;
            dirswitch = !dirswitch;
        }
        return direction;
    }

    public static double minWallDistance(Bot b) {
        if (!b.exiting()) {
            return Map.getRoom(b.getRoomID()).minDistanceToWall(b) - botRadius;
        }
        if (b.onStairs()) {
            return b.getStaircase().minDistanceToWall(b) - botRadius;
        }
        return b.getCurrentWaypoint().getRect().minDistanceToWall(b.getX(), b.getY()) - botRadius;
    }

    /**
     * Tests if the bot can move in the direction without crashing
     * @param b The bot
     * @param direction The direction
     * @return If the bot can move in the direction without crashing
     */
    public static boolean canBotMoveInDirection(Bot b, double direction) {
        b.setDirection(direction);
        double newBotX = b.calculateNewXCoord();
        double newBotY = b.calculateNewYCoord();
        return legitMove(b, newBotX, newBotY);
    }

    public static boolean legitMove(Bot b, double x, double y) {
        if (!(b.getRoomID() == 0 || (b.onStairs() && b.getStaircase().onStaircaseLegit(b)) ||
                (!b.onStairs() && Map.getRoom(b.getRoomID()).botInRoom(x, y, b.getCurrentWaypoint())))) {
            return false;
        }
        for (int dNum = 0; dNum < b.getNumImmediateNeighbors(); dNum++) {
            Bot d = getBot(b.getImmediateNeighbor(dNum));
            //if (!(distance(newBotX, newBotY, d.calculateNewXCoord(), d.calculateNewYCoord()) <= 6)) {
            //  return true;
            //if (distance(newBotX, newBotY, d.getX(), d.getY()/*d.calculateNewXCoord(), d.calculateNewYCoord()*/) <= BotAI.botRadius * 2 &&
            if (inRange(x, y, d.getX(), d.getY(), BotAI.botRadius * 2)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Moves all the bots
     */
    public static void moveBots() {
        for (Bot bot : bots) {
            if (!bot.escaped()) {
                if (bot.transferringToStaircase()) {
                    if (bot.canMoveOntoStairs()) {
                        bot.moveOntoStairs();
                    }
                } else {
                    //if (!bot.exiting()) {
                    if (!bot.onStairs()) {
                        bot.setDestinations();
                    }
                    double direction = getNearestPossibleDirection(bot, getMaxAdvantageousAngle(bot));
                    bot.setDirection(direction);
                    /*} else {
                    bot.setVelocity(defaultVelocity);
                    }*/
                    bot.move();
                }
            }
        }
    }

    /**
     * Checks the bots for any deaths. Deaths occur when a bot has been in the same exit for over deathTime iterations.
     */
    public static void checkForDeath() {
        for (Bot bot : bots) {
            if (bot.getHowLongSameRoom() > deathTime) {
                bot.die();
            }
        }
    }

    /**
     * Gets the nearest neighbor to the bot
     * @param bot1 The bot
     * @return The nearest neighbor
     */
    public static Bot getNearestNeighbor(Bot bot1) {
        double shortestDistance = Double.POSITIVE_INFINITY;
        Bot nearest = null;
        for (int bot2Index = 0; bot2Index < bot1.getNumImmediateNeighbors(); bot2Index++) {
            Bot bot2 = getBot(bot1.getImmediateNeighbor(bot2Index));
            if (bot2.equals(bot1)) {
                Logger.getLogger("FireEscapeLog").log(Level.FINE, "Bot list management");
                //System.out.println("BotAI getNearestNeighbor");
            }
            double distance = distance(bot1, bot2);
            if (distance < shortestDistance) {
                shortestDistance = distance;
                nearest = bot2;
            }
        }
        return nearest;
    }

    /**
     * Generates the neighbors
     */
    public static void generateNeighbors() {
        for (Bot b : bots) {
            b.resetNeighbors();
        }
        /*for (int floorIndex = 0; floorIndex < Map.getNumFloors(); floorIndex++) {
        Floor floor = Map.getFloor(floorIndex);
        for (int roomIndex = 0; roomIndex < floor.getNumRooms(); roomIndex++) {
        Room room = floor.getRoom(roomIndex);
        for (int botIndex = 0; botIndex < room.getNumBots(); botIndex++) {   //generating immediate neighbors
        Bot bot = room.getBot(botIndex);
        bot.resetNeighbors();
        }
        }
        }*/
        for (int floorIndex = 0; floorIndex < Map.getNumFloors(); floorIndex++) {
            Floor floor = Map.getFloor(floorIndex);
            for (int roomIndex = 0; roomIndex < floor.getNumRooms(); roomIndex++) {
                Room room = floor.getRoom(roomIndex);
                for (int bot1Index = 0; bot1Index < room.getNumBots(); bot1Index++) {   //generating immediate neighbors
                    Bot bot1 = room.getBot(bot1Index);
                    for (int bot2Index = bot1Index + 1; bot2Index < room.getNumBots(); bot2Index++) {
                        Bot bot2 = room.getBot(bot2Index);
                        checkNeighbors(bot1, bot2, floorIndex);
                    }
                }
            }
            /*for (int exitIndex = 0; exitIndex < floor.getNumExits(); exitIndex++) {
            Exit exit = floor.getExit(exitIndex);
            if (!(exit instanceof Staircase)) {
            for (int bot1Index = 0; bot1Index < exit.getNumBotsApproaching(1); bot1Index++) {   //generating immediate neighbors
            Bot bot1 = exit.getBotApproaching(bot1Index, 1);
            for (int bot2Index = 0; bot2Index < exit.getNumBotsApproaching(2); bot2Index++) {
            Bot bot2 = exit.getBotApproaching(bot2Index, 2);
            if (bot1.equals(bot2)) {
            System.out.println("This is phailyeure");
            }
            double distance = distance(bot1, bot2);
            if (distance <= 2 * botRadius + 2 * defaultDistance) {
            bot1.addImmediateNeighbor(bot2.getBotNum());
            bot2.addImmediateNeighbor(bot1.getBotNum());
            }
            }
            }
            }
            }*/
        }
        for (int stairIndex = 0; stairIndex < Map.getNumStaircases(); stairIndex++) {
            Staircase staircase = Map.getStaircase(stairIndex);
            for (int bot1Index = 0; bot1Index < staircase.getNumBots(); bot1Index++) {
                Bot bot1 = staircase.getBot(bot1Index);
                for (int bot2Index = bot1Index + 1; bot2Index < staircase.getNumBots(); bot2Index++) {
                    Bot bot2 = staircase.getBot(bot2Index);
                    checkNeighbors(bot1, bot2, -1);
                }
            }
        }
        //checkNeighbors();
    }

    public static void checkNeighbors(Bot bot1, Bot bot2, int floorIndex) {
        if (bot1.equals(bot2)) {
            Logger.getLogger("FireEscapeLog").log(Level.FINE, "Bot list management");
            //System.out.println("BotAI checkNeighbors1");
        }
        if (Map.getRoom(bot2.getRoomID()).getFloorNum() == floorIndex ||
                (Map.enableWindowJumping && bot2.getRoomID() == 0) ||
                floorIndex == -1) {    //bots that are in the staircase are still in this list
            if (inRange(bot1, bot2, 2 * botRadius + 2 * defaultDistance)) {
                bot1.addImmediateNeighbor(bot2.getBotNum());
                bot2.addImmediateNeighbor(bot1.getBotNum());
            }
        } else {
            Logger.getLogger("FireEscapeLog").log(Level.FINE, "Bots should be on same floor");
            //System.out.println("BotAI checkNeighbors2");
        }
    }

    /**
     * Returns if the bots are within a certain range of each other
     * @param bot1 The first bot
     * @param bot2 The second bot
     * @param maxDistance The certain range
     * @return True if the distance between the bots is smaller than the range, false if not
     */
    public static boolean inRange(Bot bot1, Bot bot2, double maxDistance) {
        return inRange(bot1.getX(), bot1.getY(), bot2.getX(), bot2.getY(), maxDistance);
    }

    /**
     * Returns if the coordinates are within a certain range of each other
     * @param x1 The first X coordinate
     * @param y1 The first Y coordinate
     * @param x2 The second X coordinate
     * @param y2 The second Y coordinate
     * @param maxDistance The certain range
     * @return True if the distance between the bots is smaller than the range, false if not
     */
    public static boolean inRange(double x1, double y1, double x2, double y2, double maxDistance) {
        if (Math.abs(x1 - x2) > maxDistance ||
                Math.abs(y1 - y2) > maxDistance) {  //there's no reason to compute the distance if it's obviously not in the range.
            return false;
        }
        return FireEscapeGUI.distance(x1, y1, x2, y2) <= maxDistance;
    }

    /*public static void checkNeighbors() {
    for (int j = 0; j < Map.getNumRooms(); j++) {
    Room room = Map.getRoom(j);
    for (int k = 0; k < room.getNumBots(); k++) {
    Bot bot1 = room.getBot(k);
    for (int l = 0; l < bot1.getNumImmediateNeighbors(); l++) {
    Bot bot2 = getBot(bot1.getImmediateNeighbors(l));
    int floorNum = Map.getRoom(bot2.getRoomID()).getFloorNum();
    if (floorNum != room.getFloorNum()) {
    System.out.println(bot1.getBotNum() + "  " + bot2.getBotNum());
    }
    }
    }
    }
    System.out.println("rawrklj");
    for (Bot bot1 : bots) {
    ArrayList<Integer> neighbors = bot1.getImmediateNeighbors();
    int floorNumz = Map.getRoom(bot1.getRoomID()).getFloorNum();
    for (int bot2Num : neighbors) {
    Bot bot2 = getBot(bot2Num);
    int floorNum = Map.getRoom(bot2.getRoomID()).getFloorNum();
    if (floorNum != floorNumz) {
    System.out.println(floorNumz + "  " + floorNum);
    }
    }
    }
    }*/

    /*@Deprecated
    public static boolean botCanMove(Bot bot, ArrayList<Bot> neighbors) {
    double newBotX = bot.calculateNewXCoord();
    double newBotY = bot.calculateNewYCoord();
    for (Bot b : neighbors) {
    //if (((newBotX - calculateNewXCoord(b)) <= 5) && ((newBotY - calculateNewYCoord(b)) <= 5)) {
    if (distance(newBotX, newBotY, b.calculateNewXCoord(), b.calculateNewYCoord()) <= 6) {
    return false;
    }
    }
    return true;
    }*/
    //public static ArrayList<Bot> getBotNeighbors(Bot b) {
        /*Room f = Map.getRoom(b.getRoomID());
    ArrayList<Bot> roomBots = new ArrayList<Bot>();
    for (int k = 0; k < f.getNumBots(); k++) {
    roomBots.add(f.getBot(k));
    }
    return roomBots;*/
    //    return Map.getRoom(b.getRoomID()).getBotList();
    //}

    /*public static ArrayList<Bot> getImmediateNeighbors(Bot bot) {
    ArrayList<Bot> neighbors = getBotNeighbors(bot);
    ArrayList<Bot> immedNeighbors = new ArrayList<Bot>();
    for (int i = neighbors.size() - 1; i >= 0; i--) {
    if (distance(bot, neighbors.get(i)) <= 6) {
    immedNeighbors.add(neighbors.get(i));
    }
    }
    return immedNeighbors;
    }*/
    /** public static double getNearestPossibleDirection(Bot b, double direction) {
    boolean dirswitch = true;
    int power = 0;
    b.setVelocity(defaultVelocity);
    while (!canBotMoveInDirection(b, direction)) {
    if (dirswitch) {
    direction += Math.PI / 180 * power;
    } else {
    direction -= Math.PI / 180 * power;
    }
    if (power >= 360) {
    Bot nearNeigh = b.getNearestNeighbor();
    double distance = distance(b.getX(), b.getY(), nearNeigh.getX(), nearNeigh.getY());
    b.setVelocity(distance / timePerIteration);// + defaultVelocity * 0.4);
    return calculateBotAngle(b, nearNeigh) + Math.PI;//((.5 + Math.random()) * Math.PI);
    }
    power++;
    dirswitch = !dirswitch;
    }
    return direction;
    }**/

    /*public static Bot getNearestNeighbor(Bot b, ArrayList<Bot> neighbors) {
    if (neighbors.size() == 0) {
    return null;
    }
    double x = b.getX();
    double y = b.getY();
    Bot d = neighbors.get(0);
    Bot bot = neighbors.get(0);
    double shortestDist = distance(x, y, bot.getX(), bot.getY());
    for (int i = 1; i < neighbors.size(); i++) {
    bot = neighbors.get(i);
    if (Math.abs(x - bot.getX()) <= shortestDist && Math.abs(y - bot.getY()) <= shortestDist) {
    double distance = distance(x, y, bot.getX(), bot.getY());
    if (distance < shortestDist) {
    d = bot;
    shortestDist = distance;
    }
    }
    }
    return d;
    }*/
    /**public static boolean canBotMoveInDirection(Bot b, double direction) {
    ArrayList<Bot> neighbors = b.getImmediateNeighbors();
    double newBotX = b.getX() + (b.getVelocity() * Math.cos(direction) * BotAI.timePerIteration);
    double newBotY = b.getY() + (b.getVelocity() * Math.sin(direction) * BotAI.timePerIteration);
    for (Bot d : neighbors) {
    //if (!(distance(newBotX, newBotY, d.calculateNewXCoord(), d.calculateNewYCoord()) <= 6)) {
    //  return true;
    if (d != b &&
    distance(newBotX, newBotY, d.calculateNewXCoord(), d.calculateNewYCoord()) <= 6 &&
    Map.getRoom(b.getRoomID()).botInRoom(newBotX, newBotY)) {
    return false;
    }
    }
    return true;
    }**/
}
